<?php
/**
 * Message
 * 
 * @category	XML
 * @package		dayscript.xml.rpc
 * @subpackage	
 * @author		Nelson Daza <ndaza@dayscript.com>
 * @copyright	2009 Dayscript Ltda.
 * @license		
 * @version		1.0
 * @version		$Revision: 0 $
 * @filesource	
 * @link		http://www.dayscript.com
 * @link		{docsLink}
 * @uses		
 * @since		1.0
 * @modifiedby	$LastChangedBy: Nelson Daza $
 * @modified	$Date: 2009-08-13 $
 */

namespace dayscript\xml\rpc;

/**
 * Message
 *
 * @category	XML
 * @package		dayscript.xml.rpc
 * @subpackage	
 * @author		Nelson Daza <ndaza@dayscript.com>
 * @copyright	2009 Dayscript Ltda.
 * @license		
 * @version		1.0
 * @link		http://www.dayscript.com
 * @link		{docsLink}
 * @uses		
 * @since		1.0
 * @modifiedby	$LastChangedBy: Nelson Daza $
 * @modified	$Date: 2009-08-13 $
 */
class Message	{
	/**
	 * RPC Message Type
	 * @var string 
	 */
	const TYPE_FAULT = 'fault';
	/**
	 * String message
	 * @var string $message
	 */
	private $message;
	/**
	 * methodCall / methodResponse / fault
	 * @var string
	 */
	private $type;
	/**
	 * Message fault code
	 * @var int
	 */
	private $faultCode;
	/**
	 * Message fault string
	 * @var string
	 */
	private $faultString;
	/**
	 * Method name
	 * @var string
	 */
	private $methodName;
	/**
	 * Message parameters
	 * @var array
	 */
	private $params;
	/**
	 * Stack used to keep track of the current array/struct 
	 * @var array
	 */
	private $_arraystructs = array ( );
	/**
	 * Stack used to keep track of types array/struct 
	 * @var array
	 */
	private $_arraystructstypes = array ( ); // Stack keeping track of if things are structs or array
	/**
	 * Stack used to keep track of names 
	 * @var array
	 */
	private $_currentStructName = array ( ); // A stack as well
	/**
	 * Message tag param 
	 * @var string
	 */
	private $_param;
	/**
	 * Message tag value 
	 * @var mixed
	 */
	private $_value;
	/**
	 * Message current tag 
	 * @var string
	 */
	private $_currentTag;
	/**
	 * Message current tag content 
	 * @var string
	 */
	private $_currentTagContents;
	/**
	 * The XML parser 
	 * @var string
	 */
	private $_parser;
	/**
	 * Message constructor
	 * @param string $message
	 * @return Message
	 */
	public function __construct( $message )	{
		$this->message = $message;
	}
	/**
	 * Returns the actual params
	 * @return array
	 */
	public function getParams( )	{
		return $this->params;
	}
	/**
	 * Returns the actual params
	 * @return array
	 */
	public function getType( )	{
		return $this->type;
	}
	/**
	 * Returns the actual method name
	 * @return array
	 */
	public function getMethodName( )	{
		return $this->methodName;
	}
	/**
	 * XML message parser
	 * @return bool $parsed Whether or not parser is success.
	 */
	public function parse( )	{
		// first remove the Headers if any
		$this->message = \substr( $this->message, \strpos( $this->message, '<' ) );
		// second remove the XML declaration
		$this->message = \trim( \preg_replace( '/<\\?xml(.*)\\?>/', '', $this->message ) );
		if ( $this->message == '' )
			return false;
		
		$this->_parser = \xml_parser_create( );
		// Set XML parser to take the case of tags in to account
		\xml_parser_set_option( $this->_parser, XML_OPTION_CASE_FOLDING, false );
		// Set XML parser callback functions
		\xml_set_object( $this->_parser, $this );
		\xml_set_element_handler( $this->_parser, 'tagOpen', 'tagClose' );
		\xml_set_character_data_handler( $this->_parser, 'cdata' );
		
		if ( !xml_parse( $this->_parser, $this->message ) )
			throw new \dayscript\xml\rpc\Exception ( \sprintf( "XML error: %s at line %d in message:\n%s\n", \xml_error_string( \xml_get_error_code( $this->_parser ) ), \xml_get_current_line_number( $this->_parser ), $this->message ) );

		\xml_parser_free ( $this->_parser );
		// Grab the error messages, if any
		if ( $this->type == self::TYPE_FAULT )	{
			$this->faultCode = $this->params[0]['faultCode'];
			$this->faultString = $this->params[0]['faultString'];
		}
		return true;
	}
	/**
	 * The element handler function for the XML parser. start_element_handler
	 * @param XMLParser $parser Reference to the XML parser calling the handler. 
	 * @param string $tag Name of the element for which this handler is called. If case-folding is in effect for this parser, the element name will be in uppercase letters. 
	 * @param mixed $attr Associative array with the element's attributes (if any). The keys of this array are the attribute names, the values are the attribute values. Attribute names are case-folded on the same criteria as element names. Attribute values are not case-folded. The original order of the attributes can be retrieved by walking through attribs the normal way, using each(). The first key in the array was the first attribute, and so on. 
	 */
	public function tagOpen( $parser, $tag, $attr )	{
		$this->currentTag = $tag;
		switch ( $tag )	{
			case 'methodCall' :
			case 'methodResponse' :
			case 'fault' :
				$this->type = $tag;
				break;
			/* Deal with stacks of arrays and structs */
			case 'data' : // data is to all intents and puposes more interesting than array
				$this->_arraystructstypes[] = 'array';
				$this->_arraystructs[] = array ();
				break;
			case 'struct' :
				$this->_arraystructstypes[] = 'struct';
				$this->_arraystructs[] = array ();
				break;
			default:
				break;
		}
	}
	/**
	 * 
	 * @param $parser
	 * @param $cdata
	 * @return unknown_type
	 */
	public function cdata( $parser, $cdata )	{
		$this->_currentTagContents .= $cdata;
	}
	/**
	 * The element handler function for the XML parser. end_element_handler
	 * @param XMLParser $parser Reference to the XML parser calling the handler. 
	 * @param string $tag Name of the element for which this handler is called. If case-folding is in effect for this parser, the element name will be in uppercase letters. 
	 */
	public function tagClose( $parser, $tag )	{
		$valueFlag = false;
		switch ( $tag )	{
			case 'int' :
			case 'i4' :
				$value = (int)\trim( $this->_currentTagContents );
				$this->_currentTagContents = '';
				$valueFlag = true;
				break;
			case 'double' :
				$value = (double)\trim( $this->_currentTagContents );
				$this->_currentTagContents = '';
				$valueFlag = true;
				break;
			case 'string' :
				$value = (string)\trim( $this->_currentTagContents );
				$this->_currentTagContents = '';
				$valueFlag = true;
				break;
			case 'dateTime.iso8601' :
				$value = new \dayscript\xml\rpc\data\Date( \trim( $this->_currentTagContents ) );
				// $value = $iso->getTimestamp();
				$this->_currentTagContents = '';
				$valueFlag = true;
				break;
			case 'value' :
				// "If no type is indicated, the type is string."
				if (trim ( $this->_currentTagContents ) != '') {
					$value = ( string ) $this->_currentTagContents;
					$this->_currentTagContents = '';
					$valueFlag = true;
				}
				break;
			case 'boolean' :
				$value = (boolean)\trim( $this->_currentTagContents );
				$this->_currentTagContents = '';
				$valueFlag = true;
				break;
			case 'base64' :
				$value = \base64_decode( $this->_currentTagContents );
				$this->_currentTagContents = '';
				$valueFlag = true;
				break;
			/* Deal with stacks of arrays and structs */
			case 'data' :
			case 'struct' :
				$value = \array_pop( $this->_arraystructs );
				\array_pop( $this->_arraystructstypes );
				$valueFlag = true;
				break;
			case 'member' :
				\array_pop( $this->_currentStructName );
				break;
			case 'name' :
				$this->_currentStructName [] = \trim( $this->_currentTagContents );
				$this->_currentTagContents = '';
				break;
			case 'methodName' :
				$this->methodName = \trim( $this->_currentTagContents );
				$this->_currentTagContents = '';
				break;
		}
		if ( $valueFlag )	{
            if ( !\is_array( $value ) && !\is_object( $value ) )
                $value = \trim( $value );
			if ( \count( $this->_arraystructs ) > 0) {
				// Add value to struct or array
				if ($this->_arraystructstypes[ \count( $this->_arraystructstypes ) - 1] == 'struct') {
					// Add to struct
					$this->_arraystructs[ \count( $this->_arraystructs ) - 1] [$this->_currentStructName[ \count( $this->_currentStructName ) - 1]] = $value;
				} else {
					// Add to array
					$this->_arraystructs[ \count( $this->_arraystructs ) - 1] [] = $value;
				}
			} else {
				// Just add as a paramater
				$this->params [] = $value;
			}
		}
	}       
}
